diff options
Diffstat (limited to 'app/[lng]/evcp/(evcp)/menu-access-dept/_components')
4 files changed, 0 insertions, 1084 deletions
diff --git a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx deleted file mode 100644 index f8a75641..00000000 --- a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx +++ /dev/null @@ -1,380 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Loader2, Users, Building2, AlertCircle } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Textarea } from "@/components/ui/textarea"; -import { Badge } from "@/components/ui/badge"; -import { Label } from "@/components/ui/label"; -import { Separator } from "@/components/ui/separator"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { - DepartmentNode -} from "@/lib/users/knox-service"; -import { - getDepartmentDomainAssignmentsByDepartments -} from "@/lib/users/department-domain/service"; -import { DOMAIN_OPTIONS, getDomainLabel } from "./domain-constants"; - -interface ExistingAssignment { - id: number; - companyCode: string; - departmentCode: string; - departmentName: string; - assignedDomain: string; - description?: string | null; - createdAt: Date; - updatedAt: Date; -} - -interface DepartmentDomainAssignmentDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - selectedDepartments: string[]; - departments: DepartmentNode[]; - companyInfo: { code: string; name: string }; - onAssign: (assignments: { - departmentCodes: string[]; - domain: string; - description?: string; - }) => Promise<void>; - isLoading?: boolean; -} - -export function DepartmentDomainAssignmentDialog({ - open, - onOpenChange, - selectedDepartments, - departments, - companyInfo, - onAssign, - isLoading = false, -}: DepartmentDomainAssignmentDialogProps) { - const [selectedDomain, setSelectedDomain] = React.useState<string>(""); - const [description, setDescription] = React.useState<string>(""); - const [isSubmitting, setIsSubmitting] = React.useState(false); - const [existingAssignments, setExistingAssignments] = React.useState<ExistingAssignment[]>([]); - const [isLoadingAssignments, setIsLoadingAssignments] = React.useState(false); - - // 선택된 부서들의 정보 가져오기 - const getSelectedDepartmentInfo = React.useCallback(() => { - const findDepartment = (nodes: DepartmentNode[], code: string): DepartmentNode | null => { - for (const node of nodes) { - if (node.departmentCode === code) { - return node; - } - const found = findDepartment(node.children, code); - if (found) return found; - } - return null; - }; - - return selectedDepartments - .map(code => findDepartment(departments, code)) - .filter(Boolean) as DepartmentNode[]; - }, [departments, selectedDepartments]); - - // 회사별로 그룹화 - const selectedDepartmentsByCompany = React.useMemo(() => { - const deptInfo = getSelectedDepartmentInfo(); - const grouped = new Map<string, DepartmentNode[]>(); - - deptInfo.forEach(dept => { - if (!grouped.has(dept.companyCode)) { - grouped.set(dept.companyCode, []); - } - grouped.get(dept.companyCode)!.push(dept); - }); - - return grouped; - }, [getSelectedDepartmentInfo]); - - // 기존 할당 정보 조회 - React.useEffect(() => { - if (open && selectedDepartments.length > 0) { - const loadExistingAssignments = async () => { - setIsLoadingAssignments(true); - try { - const assignments = await getDepartmentDomainAssignmentsByDepartments(selectedDepartments); - setExistingAssignments(assignments as ExistingAssignment[]); - } catch (error) { - console.error("기존 할당 정보 조회 실패:", error); - setExistingAssignments([]); - } finally { - setIsLoadingAssignments(false); - } - }; - - loadExistingAssignments(); - } else { - setExistingAssignments([]); - } - }, [open, selectedDepartments]); - - // 폼 초기화 - React.useEffect(() => { - if (open) { - setSelectedDomain(""); - setDescription(""); - setIsSubmitting(false); - } - }, [open]); - - // 할당 처리 - const handleAssign = async () => { - if (!selectedDomain || selectedDepartments.length === 0) { - return; - } - - setIsSubmitting(true); - - try { - await onAssign({ - departmentCodes: selectedDepartments, - domain: selectedDomain, - description: description.trim() || undefined, - }); - - // 성공 시 다이얼로그 닫기 - onOpenChange(false); - } catch (error) { - console.error("도메인 할당 실패:", error); - } finally { - setIsSubmitting(false); - } - }; - - const canSubmit = selectedDomain && selectedDepartments.length > 0 && !isSubmitting && !isLoading; - const selectedDomainInfo = DOMAIN_OPTIONS.find(opt => opt.value === selectedDomain); - const hasConflicts = existingAssignments.some(a => a.assignedDomain !== selectedDomain && selectedDomain); - - return ( - <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden flex flex-col"> - <DialogHeader> - <DialogTitle className="flex items-center gap-2"> - <Building2 className="h-5 w-5" /> - 부서별 도메인 할당 - </DialogTitle> - <DialogDescription> - 선택된 {selectedDepartments.length}개 부서에 도메인을 할당합니다. - 상위 부서를 선택한 경우 하위 부서들도 자동으로 포함됩니다. - </DialogDescription> - </DialogHeader> - - <div className="flex-1 overflow-y-auto px-1"> - <div className="space-y-6 pr-3"> - {/* 선택된 부서들 표시 */} - <div className="space-y-3"> - <Label className="text-sm font-medium flex items-center gap-2"> - <Users className="h-4 w-4" /> - 선택된 부서 ({selectedDepartments.length}개) - </Label> - - <div className="border rounded-md p-3 max-h-40 overflow-y-auto"> - {Array.from(selectedDepartmentsByCompany.entries()).map(([companyCode, depts]) => ( - <div key={companyCode} className="mb-3 last:mb-0"> - <div className="text-sm font-medium text-muted-foreground mb-2"> - {companyCode} - {companyInfo.name} - </div> - <div className="flex flex-wrap gap-2"> - {depts.map((dept) => ( - <Badge - key={dept.departmentCode} - variant="outline" - className="text-xs" - > - {dept.departmentName || dept.departmentCode} - </Badge> - ))} - </div> - </div> - ))} - </div> - </div> - - {/* 기존 할당 현황 */} - {(existingAssignments.length > 0 || isLoadingAssignments) && ( - <> - <Separator /> - <div className="space-y-3"> - <Label className="text-sm font-medium flex items-center gap-2"> - <AlertCircle className="h-4 w-4" /> - 현재 할당 현황 - </Label> - - {isLoadingAssignments ? ( - <div className="flex items-center justify-center py-4"> - <Loader2 className="h-4 w-4 animate-spin mr-2" /> - 기존 할당 정보를 조회하는 중... - </div> - ) : ( - <div className="border rounded-md max-h-60 overflow-y-auto"> - <Table> - <TableHeader> - <TableRow> - <TableHead>부서</TableHead> - <TableHead>현재 도메인</TableHead> - <TableHead>할당일</TableHead> - <TableHead>설명</TableHead> - </TableRow> - </TableHeader> - <TableBody> - {existingAssignments.map((assignment) => ( - <TableRow key={assignment.id}> - <TableCell className="font-medium"> - {assignment.departmentName} - </TableCell> - <TableCell> - <Badge - variant={assignment.assignedDomain === 'evcp' ? 'default' : 'secondary'} - > - {getDomainLabel(assignment.assignedDomain)} - </Badge> - </TableCell> - <TableCell className="text-sm text-muted-foreground"> - {new Date(assignment.createdAt).toLocaleDateString('ko-KR')} - </TableCell> - <TableCell className="max-w-xs truncate text-sm"> - {assignment.description || '-'} - </TableCell> - </TableRow> - ))} - </TableBody> - </Table> - </div> - )} - - {hasConflicts && ( - <div className="bg-yellow-50 border-yellow-200 border rounded-md p-3"> - <div className="flex items-start gap-2"> - <AlertCircle className="h-4 w-4 text-yellow-600 mt-0.5" /> - <div className="text-sm"> - <div className="font-medium text-yellow-800">도메인 변경 주의</div> - <div className="text-yellow-700"> - 일부 부서의 기존 도메인과 다른 도메인을 할당하려고 합니다. - 기존 할당은 자동으로 비활성화됩니다. - </div> - </div> - </div> - </div> - )} - </div> - </> - )} - - <Separator /> - - {/* 도메인 선택 */} - <div className="space-y-2"> - <Label htmlFor="domain-select" className="text-sm font-medium"> - 할당할 도메인 * - </Label> - <Select value={selectedDomain} onValueChange={setSelectedDomain}> - <SelectTrigger id="domain-select"> - <SelectValue placeholder="도메인을 선택하세요" /> - </SelectTrigger> - <SelectContent> - {DOMAIN_OPTIONS.map((option) => ( - <SelectItem key={option.value} value={option.value}> - <div className="flex flex-col"> - <span className="font-medium">{option.label}</span> - <span className="text-xs text-muted-foreground"> - {option.description} - </span> - </div> - </SelectItem> - ))} - </SelectContent> - </Select> - - {selectedDomainInfo && ( - <div className="text-sm text-muted-foreground"> - <Badge variant="secondary" className="mr-2"> - {selectedDomainInfo.label} - </Badge> - {selectedDomainInfo.description} - </div> - )} - </div> - - {/* 할당 사유/설명 */} - <div className="space-y-2"> - <Label htmlFor="description" className="text-sm font-medium"> - 할당 사유 또는 설명 (선택사항) - </Label> - <Textarea - id="description" - placeholder="예: 구매 업무 담당자들에게 procurement 도메인 할당" - value={description} - onChange={(e) => setDescription(e.target.value)} - rows={3} - maxLength={500} - /> - <div className="text-xs text-muted-foreground text-right"> - {description.length}/500 - </div> - </div> - - {/* 주의사항 */} - <div className="bg-muted/50 p-3 rounded-md"> - <div className="text-sm text-muted-foreground"> - <div className="font-medium mb-1">⚠️ 주의사항</div> - <ul className="list-disc list-inside space-y-1 text-xs"> - <li>도메인 할당은 해당 부서 소속 사용자들의 메뉴 접근 권한에 영향을 줍니다.</li> - <li>기존에 다른 도메인이 할당된 부서는 새로운 도메인으로 덮어씌워집니다.</li> - <li>Knox 조직도 변경으로 인해 부서가 삭제된 경우, 해당 할당은 고립된 레코드가 됩니다.</li> - </ul> - </div> - </div> - </div> - </div> - - <DialogFooter className="border-t pt-4"> - <Button - variant="outline" - onClick={() => onOpenChange(false)} - disabled={isSubmitting || isLoading} - > - 취소 - </Button> - <Button - onClick={handleAssign} - disabled={!canSubmit} - > - {isSubmitting || isLoading ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - 할당 중... - </> - ) : ( - `도메인 할당 (${selectedDepartments.length}개 부서)` - )} - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - ); -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx deleted file mode 100644 index c24770bf..00000000 --- a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx +++ /dev/null @@ -1,309 +0,0 @@ -"use client"; - -import * as React from "react"; -import { useState, useTransition, useEffect } from "react"; -import { Settings, Plus, Users } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { toast } from "sonner"; -import { DepartmentTreeView } from "./department-tree-view"; -import { DepartmentDomainAssignmentDialog } from "./department-domain-assignment-dialog"; -import { - type DepartmentNode -} from "@/lib/users/knox-service"; -import { - assignDomainToDepartments, - getDepartmentDomainAssignments, - autoAssignPendingUsersDomains, - type UserDomain -} from "@/lib/users/department-domain/service"; -import { DOMAIN_OPTIONS } from "./domain-constants"; - -interface DepartmentMenuAccessManagerProps { - departmentsPromise: Promise<DepartmentNode[]>; - companyInfo: { code: string; name: string }; -} - -interface DepartmentAssignment { - id: number; - departmentCode: string; - departmentName: string; - assignedDomain: string; - description?: string | null; -} - -export function DepartmentMenuAccessManager({ - departmentsPromise, - companyInfo -}: DepartmentMenuAccessManagerProps) { - const [departments, setDepartments] = useState<DepartmentNode[]>([]); - const [selectedDepartments, setSelectedDepartments] = useState<string[]>([]); - const [assignments, setAssignments] = useState<DepartmentAssignment[]>([]); - const [isDialogOpen, setIsDialogOpen] = useState(false); - const [isPending, startTransition] = useTransition(); - const [isDepartmentsLoading, setIsDepartmentsLoading] = useState(true); - const [isAssignmentsLoading, setIsAssignmentsLoading] = useState(true); - - // Promise를 해결하여 부서 데이터 로드 - useEffect(() => { - const loadDepartments = async () => { - setIsDepartmentsLoading(true); - try { - const departmentTree = await departmentsPromise; - setDepartments(departmentTree); - } catch (error) { - console.error("부서 트리 로드 실패:", error); - toast.error("부서 정보를 불러오는데 실패했습니다."); - setDepartments([]); - } finally { - setIsDepartmentsLoading(false); - } - }; - - loadDepartments(); - }, [departmentsPromise]); - - // 기존 할당 정보 로드 - useEffect(() => { - const loadAssignments = async () => { - setIsAssignmentsLoading(true); - try { - const assignmentData = await getDepartmentDomainAssignments(); - setAssignments(assignmentData as DepartmentAssignment[]); - } catch (error) { - console.error("할당 정보 로드 실패:", error); - toast.error("할당 정보를 불러오는데 실패했습니다."); - setAssignments([]); - } finally { - setIsAssignmentsLoading(false); - } - }; - - loadAssignments(); - }, []); - - // 선택된 부서들의 정보 가져오기 - const getSelectedDepartmentInfo = React.useCallback(() => { - const findDepartment = (nodes: DepartmentNode[], code: string): DepartmentNode | null => { - for (const node of nodes) { - if (node.departmentCode === code) { - return node; - } - const found = findDepartment(node.children, code); - if (found) return found; - } - return null; - }; - - return selectedDepartments - .map(code => findDepartment(departments, code)) - .filter(Boolean) as DepartmentNode[]; - }, [departments, selectedDepartments]); - - // 도메인 할당 처리 - const handleDomainAssign = async (assignmentData: { - departmentCodes: string[]; - domain: string; - description?: string; - }) => { - // 선택된 부서들의 이름 매핑 생성 - const departmentNames: Record<string, string> = {}; - const collectDepartmentNames = (nodes: DepartmentNode[]) => { - nodes.forEach(node => { - if (assignmentData.departmentCodes.includes(node.departmentCode)) { - departmentNames[node.departmentCode] = node.departmentName || node.departmentCode; - } - collectDepartmentNames(node.children); - }); - }; - collectDepartmentNames(departments); - - startTransition(async () => { - try { - const result = await assignDomainToDepartments({ - departmentCodes: assignmentData.departmentCodes, - domain: assignmentData.domain as UserDomain, - description: assignmentData.description, - departmentNames, - }); - - if (result.success) { - toast.success(result.message); - setSelectedDepartments([]); - - // 할당 정보 새로고침 - try { - const updatedAssignments = await getDepartmentDomainAssignments(); - setAssignments(updatedAssignments as DepartmentAssignment[]); - } catch (error) { - console.error("할당 정보 새로고침 실패:", error); - } - - // users 테이블에 도메인 동기화 작업 진행 - try { - const syncResult = await autoAssignPendingUsersDomains(); - if (syncResult.success && syncResult.assignedCount > 0) { - toast.success(`사용자 도메인 동기화 완료: ${syncResult.assignedCount}명의 사용자가 자동 할당되었습니다.`); - } - } catch (error) { - console.error("사용자 도메인 동기화 실패:", error); - // 동기화 실패해도 메인 할당은 성공이므로 에러 토스트는 표시하지 않음 - } - } else { - toast.error(result.message); - } - } catch (error) { - console.error("도메인 할당 실패:", error); - toast.error("도메인 할당 중 오류가 발생했습니다."); - } - }); - }; - - const canAssign = selectedDepartments.length > 0; - const selectedDepartmentInfo = getSelectedDepartmentInfo(); - - const isLoading = isDepartmentsLoading || isAssignmentsLoading; - - return ( - <div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> - {/* 왼쪽: 조직도 트리 */} - <div className="lg:col-span-2"> - <Card> - <CardHeader> - <CardTitle className="flex items-center gap-2"> - <Settings className="h-5 w-5" /> - 조직도 - {companyInfo.name} - </CardTitle> - <CardDescription> - 부서를 선택하여 도메인을 할당하세요. 상위 부서 선택 시 하위 부서들도 자동으로 포함됩니다. - </CardDescription> - </CardHeader> - <CardContent className="p-0"> - {isLoading ? ( - <div className="flex items-center justify-center h-[80vh]"> - <div className="text-center"> - <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div> - <p className="text-muted-foreground">조직도를 불러오는 중...</p> - </div> - </div> - ) : ( - <DepartmentTreeView - departments={departments} - selectedDepartments={selectedDepartments} - onSelectionChange={setSelectedDepartments} - assignments={assignments} - /> - )} - </CardContent> - </Card> - </div> - - {/* 오른쪽: 선택된 부서 정보 및 할당 버튼 */} - <div className="space-y-6"> - {/* 선택된 부서 정보 */} - <Card> - <CardHeader> - <CardTitle className="flex items-center gap-2"> - <Users className="h-5 w-5" /> - 선택된 부서 - </CardTitle> - <CardDescription> - {selectedDepartments.length}개 부서가 선택되었습니다 - </CardDescription> - </CardHeader> - <CardContent> - {selectedDepartmentInfo.length === 0 ? ( - <div className="text-center py-4 text-muted-foreground"> - 부서를 선택해주세요 - </div> - ) : ( - <div className="space-y-2 max-h-60 overflow-y-auto"> - {selectedDepartmentInfo.map((dept) => { - const assignment = assignments.find(a => a.departmentCode === dept.departmentCode); - return ( - <div - key={dept.departmentCode} - className="flex items-center justify-between p-2 bg-accent/20 rounded-md" - > - <div className="min-w-0"> - <div className="font-medium truncate"> - {dept.departmentName || dept.departmentCode} - </div> - <div className="text-xs text-muted-foreground"> - {dept.departmentCode} - </div> - </div> - {assignment && ( - <Badge variant="outline" className="text-xs shrink-0"> - {assignment.assignedDomain} - </Badge> - )} - </div> - ); - })} - </div> - )} - </CardContent> - </Card> - - {/* 도메인 할당 버튼 */} - <Card> - <CardHeader> - <CardTitle className="text-lg">도메인 할당</CardTitle> - <CardDescription> - 선택된 부서들에 도메인을 할당합니다 - </CardDescription> - </CardHeader> - <CardContent> - <Button - onClick={() => setIsDialogOpen(true)} - disabled={!canAssign || isPending} - size="lg" - className="w-full" - > - <Plus className="mr-2 h-4 w-4" /> - 도메인 할당 ({selectedDepartments.length}개 부서) - </Button> - - {canAssign && ( - <div className="mt-3 text-sm text-muted-foreground"> - 상위 부서를 선택한 경우 하위 부서들도 자동으로 동일한 도메인이 할당됩니다. - </div> - )} - </CardContent> - </Card> - - {/* 범례 */} - <Card> - <CardHeader> - <CardTitle className="text-lg">도메인 범례</CardTitle> - </CardHeader> - <CardContent> - <div className="grid grid-cols-1 gap-2 text-sm"> - {DOMAIN_OPTIONS.map((option) => ( - <div key={option.value} className="flex items-center gap-2"> - <Badge className={option.color}> - {option.value} - </Badge> - <span>{option.description}</span> - </div> - ))} - </div> - </CardContent> - </Card> - </div> - - {/* 도메인 할당 다이얼로그 */} - <DepartmentDomainAssignmentDialog - open={isDialogOpen} - onOpenChange={setIsDialogOpen} - selectedDepartments={selectedDepartments} - departments={departments} - companyInfo={companyInfo} - onAssign={handleDomainAssign} - isLoading={isPending} - /> - </div> - ); -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-tree-view.tsx b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-tree-view.tsx deleted file mode 100644 index 126f1eb7..00000000 --- a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-tree-view.tsx +++ /dev/null @@ -1,343 +0,0 @@ -"use client"; - -import * as React from "react"; -import { ChevronDown, ChevronRight, Minus, Plus } from "lucide-react"; -import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Badge } from "@/components/ui/badge"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { DepartmentNode } from "@/lib/users/knox-service"; -import { getDomainLabel, getDomainColor } from "./domain-constants"; - -interface DepartmentAssignment { - id: number; - departmentCode: string; - assignedDomain: string; - description?: string | null; -} - -interface DepartmentTreeViewProps { - departments: DepartmentNode[]; - selectedDepartments: string[]; - onSelectionChange: (selected: string[]) => void; - assignments: DepartmentAssignment[]; - className?: string; -} - -interface TreeNodeProps { - node: DepartmentNode; - selectedDepartments: string[]; - onToggle: (departmentCode: string) => void; - expandedNodes: Set<string>; - onExpandToggle: (departmentCode: string) => void; - assignments: DepartmentAssignment[]; - level: number; -} - -function TreeNode({ - node, - selectedDepartments, - onToggle, - expandedNodes, - onExpandToggle, - assignments, - level -}: TreeNodeProps) { - const isExpanded = expandedNodes.has(node.departmentCode); - const hasChildren = node.children.length > 0; - - // 현재 부서에 할당된 도메인 찾기 - const assignment = assignments.find(a => a.departmentCode === node.departmentCode); - - // 현재 노드의 선택 상태 확인 - const isSelected = selectedDepartments.includes(node.departmentCode); - - // 하위 노드들 중 선택된 것이 있는지 확인 (부분 선택 상태 표시용) - const hasSelectedChildren = React.useMemo(() => { - if (!hasChildren) return false; - - const getAllChildCodes = (dept: DepartmentNode): string[] => { - const codes: string[] = []; - dept.children.forEach(child => { - codes.push(child.departmentCode); - codes.push(...getAllChildCodes(child)); - }); - return codes; - }; - - const childCodes = getAllChildCodes(node); - return childCodes.some(code => selectedDepartments.includes(code)); - }, [node, selectedDepartments, hasChildren]); - - const handleToggle = () => { - onToggle(node.departmentCode); - }; - - const handleExpandToggle = () => { - if (hasChildren) { - onExpandToggle(node.departmentCode); - } - }; - - return ( - <div className="select-none"> - <div - className={cn( - "flex items-center gap-2 py-2 px-3 hover:bg-accent/50 rounded-md transition-colors", - (isSelected || (!isSelected && hasSelectedChildren)) && "bg-accent/20" - )} - style={{ marginLeft: `${level * 16}px` }} - > - {/* 확장/축소 버튼 */} - <div className="flex items-center justify-center w-5 h-5"> - {hasChildren ? ( - <Button - variant="ghost" - size="sm" - className="h-5 w-5 p-0 hover:bg-transparent" - onClick={handleExpandToggle} - > - {isExpanded ? ( - <ChevronDown className="h-3 w-3" /> - ) : ( - <ChevronRight className="h-3 w-3" /> - )} - </Button> - ) : null} - </div> - - {/* 체크박스 */} - <div className="flex items-center"> - <Checkbox - checked={isSelected} - onCheckedChange={handleToggle} - className={cn( - "h-4 w-4", - !isSelected && hasSelectedChildren && "[&>*:first-child]:opacity-50" - )} - /> - </div> - - {/* 부서 정보 */} - <div className="flex-1 min-w-0 cursor-pointer" onClick={handleToggle}> - <div className="flex items-center gap-2"> - <span className={cn( - "font-medium truncate", - (isSelected || (!isSelected && hasSelectedChildren)) && "text-primary" - )}> - {node.departmentName || node.departmentCode} - </span> - - {/* 할당된 도메인 표시 */} - {assignment && ( - <Badge - className={cn( - "text-xs shrink-0", - getDomainColor(assignment.assignedDomain) - )} - variant="outline" - > - {getDomainLabel(assignment.assignedDomain)} - </Badge> - )} - </div> - - {/* 부서 코드 */} - <div className="text-xs text-muted-foreground truncate"> - {node.departmentCode} - {assignment?.description && ( - <span className="ml-1">• {assignment.description}</span> - )} - </div> - </div> - </div> - - {/* 하위 노드들 */} - {hasChildren && isExpanded && ( - <div className="mt-1"> - {node.children.map((child) => ( - <TreeNode - key={child.departmentCode} - node={child} - selectedDepartments={selectedDepartments} - onToggle={onToggle} - expandedNodes={expandedNodes} - onExpandToggle={onExpandToggle} - assignments={assignments} - level={level + 1} - /> - ))} - </div> - )} - </div> - ); -} - -export function DepartmentTreeView({ - departments, - selectedDepartments, - onSelectionChange, - assignments, - className, -}: DepartmentTreeViewProps) { - const [expandedNodes, setExpandedNodes] = React.useState<Set<string>>(new Set()); - - // 부서 토글 핸들러 - const handleToggle = (departmentCode: string) => { - const findNode = (nodes: DepartmentNode[], code: string): DepartmentNode | null => { - for (const node of nodes) { - if (node.departmentCode === code) return node; - const found = findNode(node.children, code); - if (found) return found; - } - return null; - }; - - const getAllChildCodes = (node: DepartmentNode): string[] => { - const codes: string[] = []; - node.children.forEach(child => { - codes.push(child.departmentCode); - codes.push(...getAllChildCodes(child)); - }); - return codes; - }; - - const targetNode = findNode(departments, departmentCode); - if (!targetNode) return; - - const isCurrentlySelected = selectedDepartments.includes(departmentCode); - - let newSelected: string[]; - if (isCurrentlySelected) { - // 선택 해제: 해당 부서만 제거 (하위 부서는 유지, 상위 부서에도 영향 없음) - newSelected = selectedDepartments.filter(code => code !== departmentCode); - } else { - // 선택: 해당 부서 + 모든 하위 부서 추가 - const childCodes = getAllChildCodes(targetNode); - const codesToAdd = [departmentCode, ...childCodes].filter(code => !selectedDepartments.includes(code)); - newSelected = [...selectedDepartments, ...codesToAdd]; - } - - onSelectionChange(newSelected); - }; - - // 노드 확장/축소 핸들러 - const handleExpandToggle = (departmentCode: string) => { - const newExpanded = new Set(expandedNodes); - if (newExpanded.has(departmentCode)) { - newExpanded.delete(departmentCode); - } else { - newExpanded.add(departmentCode); - } - setExpandedNodes(newExpanded); - }; - - // 전체 확장/축소 - const handleExpandAll = () => { - if (expandedNodes.size === 0) { - const getAllCodes = (nodes: DepartmentNode[]): string[] => { - const codes: string[] = []; - nodes.forEach(node => { - if (node.children.length > 0) { - codes.push(node.departmentCode); - codes.push(...getAllCodes(node.children)); - } - }); - return codes; - }; - setExpandedNodes(new Set(getAllCodes(departments))); - } else { - setExpandedNodes(new Set()); - } - }; - - // 전체 선택/해제 - const handleSelectAll = () => { - if (selectedDepartments.length === 0) { - // 전체 선택 - const allCodes: string[] = []; - const collectCodes = (nodes: DepartmentNode[]) => { - nodes.forEach(node => { - allCodes.push(node.departmentCode); - collectCodes(node.children); - }); - }; - collectCodes(departments); - onSelectionChange(allCodes); - } else { - // 전체 해제 - onSelectionChange([]); - } - }; - - return ( - <div className={cn("border rounded-lg", className)}> - {/* 헤더 */} - <div className="flex items-center justify-between p-3 border-b bg-muted/30"> - <h3 className="font-medium">조직도</h3> - <div className="flex gap-2"> - <Button - variant="outline" - size="sm" - onClick={handleExpandAll} - className="text-xs" - > - {expandedNodes.size === 0 ? ( - <> - <Plus className="mr-1 h-3 w-3" /> - 전체 펼치기 - </> - ) : ( - <> - <Minus className="mr-1 h-3 w-3" /> - 전체 접기 - </> - )} - </Button> - <Button - variant="outline" - size="sm" - onClick={handleSelectAll} - className="text-xs" - > - {selectedDepartments.length === 0 ? "전체 선택" : "선택 해제"} - </Button> - </div> - </div> - - {/* 트리 본문 */} - <ScrollArea className="h-[80vh] p-2"> - {departments.length === 0 ? ( - <div className="text-center py-8 text-muted-foreground"> - 부서 정보가 없습니다 - </div> - ) : ( - <div className="space-y-1"> - {departments.map((dept) => ( - <TreeNode - key={dept.departmentCode} - node={dept} - selectedDepartments={selectedDepartments} - onToggle={handleToggle} - expandedNodes={expandedNodes} - onExpandToggle={handleExpandToggle} - assignments={assignments} - level={0} - /> - ))} - </div> - )} - </ScrollArea> - - {/* 푸터 */} - {selectedDepartments.length > 0 && ( - <div className="border-t p-3 bg-muted/30"> - <div className="text-sm text-muted-foreground"> - 선택된 부서: <span className="font-medium text-foreground">{selectedDepartments.length}개</span> - </div> - </div> - )} - </div> - ); -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/domain-constants.ts b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/domain-constants.ts deleted file mode 100644 index 2b104d0e..00000000 --- a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/domain-constants.ts +++ /dev/null @@ -1,52 +0,0 @@ - - -// 통합된 도메인 옵션 - 모든 도메인 정보를 포함 -export const DOMAIN_OPTIONS = [ - { - value: "pending", - label: "pending", - description: "승인 대기 상태", - color: "bg-yellow-100 text-yellow-800 border-yellow-200" - }, - { - value: "evcp", - label: "evcp", - description: "eVCP 시스템 관리자", - color: "bg-blue-100 text-blue-800 border-blue-200" - }, - { - value: "procurement", - label: "procurement", - description: "구매", - color: "bg-green-100 text-green-800 border-green-200" - }, - { - value: "sales", - label: "sales", - description: "기술영업", - color: "bg-purple-100 text-purple-800 border-purple-200" - }, - { - value: "engineering", - label: "engineering", - description: "설계", - color: "bg-orange-100 text-orange-800 border-orange-200" - }, -] as const; - -// 헬퍼 함수들 - 필요시 매핑 객체 생성 -export const getDomainOption = (value: string) => { - return DOMAIN_OPTIONS.find(option => option.value === value); -}; - -export const getDomainLabel = (value: string) => { - return getDomainOption(value)?.label || value; -}; - -export const getDomainColor = (value: string) => { - return getDomainOption(value)?.color || "bg-gray-100 text-gray-800 border-gray-200"; -}; - -export const getDomainDescription = (value: string) => { - return getDomainOption(value)?.description || value; -};
\ No newline at end of file |
